/*
 * User.java
 * 
 * @version 1.0
 * CS 2340 Team 10: Cyberdyne Systems
 */

package edu.gatech.oad.cyberdoc.domain;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.json.JSONException;
import org.json.JSONObject;

import edu.gatech.oad.cyberdoc.database.DatabaseException;
import edu.gatech.oad.cyberdoc.database.DatabaseSerializable;
import edu.gatech.oad.cyberdoc.domain.action.ActionHandleException;
import edu.gatech.oad.cyberdoc.domain.action.ActionHandler;
import edu.gatech.oad.cyberdoc.domain.action.BadLoginResponse;
import edu.gatech.oad.cyberdoc.domain.filter.Filter;
import edu.gatech.oad.cyberdoc.server.Server;

/**
 * Represents a User of the server
 * 
 * @author Jessie McGarry
 * @version 1.0, March 15, 2011
 */
public abstract class User implements DatabaseSerializable {

	/**
	 * The number of login attempts to give the user before suspending the
	 * account.
	 */
	public static final int MAX_ATTEMPTS = 3;

	/**
	 * The logger for this class.
	 */
	private static final Logger LOG;

	static {
		LOG = Logger.getLogger("edu.gatech.oad.cyberdoc.domain.User");
	}

	/**
	 * Represents the four possible results of a login attempt.
	 * 
	 * @author John Girata
	 */
	public enum LoginStatus {
		GOOD, BAD_NAME, BAD_PASS, SUSPENDED
	};

	/**
	 * This user's username.
	 */
	private String username;

	/**
	 * This user's password.
	 */
	private String password;

	/**
	 * The number of consecutive unsuccessful login attempts this user has made
	 * (max 3).
	 */
	private int loginAttempts;

	/**
	 * The Map of all users currently in the system.
	 */
	private static Map<String, User> UserHash;

	/**
	 * The handlers for the actions a Patient can perform.
	 */
	private static Map<String, ActionHandler> Handlers;

	static {
		UserHash = new HashMap<String, User>();
		Handlers = new HashMap<String, ActionHandler>();
	}

	/**
	 * Logs the creation of this instance. This constructor does nothing
	 * particularly useful itself, but is chained to from the other
	 * constructors.
	 */
	private User() {
		String klass = getClass().getName();
		klass = klass.substring(klass.lastIndexOf('.') + 1);
		LOG.info("New " + klass + " created");
	}

	/**
	 * Creates a new User with the given username and password.
	 * 
	 * @param username
	 *            The User's username
	 * @param password
	 *            The User's password
	 */
	public User(String username, String password) {
		this();

		this.username = username;
		this.password = password;

		loginAttempts = 0;
		UserHash.put(username, this);
	}

	/**
	 * Creates an instance of this class from a JSONObject, as required by
	 * DatabaseSerializable.
	 * 
	 * @param object
	 *            The information from the database to use to create this User
	 */
	public User(JSONObject object) {
		this();

		username = "";
		password = "";

		try {
			if (object.has("username")) {
				username = object.getString("username");
			}

			if (object.has("password")) {
				password = object.getString("password");
			}
		} catch (JSONException e) {
			LOG.log(Level.SEVERE, e.toString(), e);
		}

		loginAttempts = 0;
		UserHash.put(username, this);
	}

	/**
	 * Receives commands from the user and delegates them to the appropriate
	 * object.
	 * 
	 * @param command
	 *            The command from the user
	 * @return The response to the command, or null if the command is null
	 * @throws ActionHandleException
	 *             If this command is improperly formatted or cannot be
	 *             completed
	 */
	public static JSONObject handleCommand(JSONObject command)
			throws ActionHandleException {
		JSONObject response = null;

		if (command == null) {
			return response;
		}

		try {
			final String username = command.getString("username");
			final User user = UserHash.get(username);

			if (user == null) {
				response = new BadLoginResponse();
			} else {
				final String action = command.getString("action");
				
				if (Handlers.containsKey(action)) {
					final ActionHandler handler = Handlers.get(command
							.getString("action"));
					response = handler.handle(user, command);
				} else {
					response = user.handle(command);
				}

				response.put("action", command.getString("action"));

				String type = user.getClass().getName().toLowerCase();
				type = type.substring(type.lastIndexOf('.') + 1);
				response.put("type", type);

				response.put("username", command.getString("username"));
			}
		} catch (JSONException e) {
			throw new ActionHandleException(e);
		} catch (UnsupportedOperationException e) {
			throw new ActionHandleException(e);
		}

		return response;
	}

	/**
	 * Defines the handler method expected of all users.
	 * 
	 * @param command
	 *            The command from the user
	 * @return The response to the command
	 * @throws UnsupportedOperationException
	 *             If the action specified in the command is not supported by
	 *             this object.
	 * @throws ActionHandleException
	 *             If an unexpected error occurs that prevents this command from
	 *             being handled
	 */
	public abstract JSONObject handle(JSONObject command)
			throws ActionHandleException, UnsupportedOperationException;

	/**
	 * Updates the user's fields to the values specified in the JSON object.
	 * 
	 * @param fields
	 *            The new values for the user's fields
	 */
	public void update(JSONObject fields) {
		try {
			if (fields.has("password")) {
				this.password = fields.getString("password");
				LOG.info(username + "'s password updated");
			}

			Server.getDatabase().save(this);
		} catch (JSONException e) {
			LOG.log(Level.SEVERE, "Unexpected exception", e);
		} catch (DatabaseException e) {
			LOG.log(Level.SEVERE, "Could not save to the database", e);
		}
	}

	/**
	 * Defines a method to allow a User instance to be serialized to a
	 * JSONObject.
	 * 
	 * @return The User instance as a JSONObject
	 * @throws JSONException
	 *             If an exception occurs while converting this instance to a
	 *             JSONObject
	 */
	public JSONObject toJSONObject() {
		final JSONObject object = new JSONObject();

		try {
			object.put("username", username);
			object.put("password", password);

			String clazz = getClass().getName();
			clazz = clazz.substring(clazz.lastIndexOf('.') + 1);
			object.put("type", clazz);
		} catch (JSONException e) {
			LOG.log(Level.SEVERE, "Unexpected JSONException.", e);
		}

		return object;
	}

	/**
	 * Returns a string that is unique to an instance of a class.
	 * 
	 * @return A unique identifier for this instance
	 */
	public String getUniqueIdentifier() {
		return username;
	}

	/**
	 * Adds an action handler to the User class.
	 * 
	 * @param action
	 *            The action code to respond to
	 * @param handler
	 *            The handler to respond to commands
	 */
	public static void addActionHandler(String action, ActionHandler handler) {
		Handlers.put(action, handler);
	}

	/**
	 * Removes an action handler that was previously registered.
	 * 
	 * @param handler
	 *            The ActionHandler to remove
	 */
	public static void removeActionHandler(ActionHandler handler) {
		// FIXME This does not work
		final Collection<ActionHandler> values = Handlers.values();

		while (values.contains(handler)) {
			values.remove(handler);
		}
	}

	/**
	 * Looks-up a user based on a username.
	 * 
	 * @param username
	 *            The username of the patient to look-up
	 * @return The User object with the corresponding username, or null if no
	 *         such User object exists
	 */
	public static User getUser(String username) {
		return UserHash.get(username);
	}

	/**
	 * Returns the list of User objects that pass all of the given filters.
	 * 
	 * @param filters
	 *            The filters to apply to the existing users
	 * @return Users meeting the given set of criteria
	 */
	public static List<User> filterUsers(Filter... filters) {
		final List<User> result = new LinkedList<User>();
		final List<User> remove = new LinkedList<User>();
		result.addAll(UserHash.values());

		for (User user : result) {
			boolean stays = true;

			for (Filter filter : filters) {
				stays &= filter.accept(user);
			}

			if (!stays) {
				remove.add(user);
			}
		}

		result.removeAll(remove);
		return result;
	}

	/**
	 * Determines if the supplied login credentials are good. If they are not,
	 * the number of failed attempts is recorded. If this number of attempts
	 * exceeds 3, the user is locked out.
	 * 
	 * @param username
	 *            The username of the user attempting to log in
	 * @param password
	 *            The password of the user attempting to log in
	 * @return An enumerated type represented the status of the login attempt
	 */
	public LoginStatus checkLogin(String username, String password) {
		if (loginAttempts >= MAX_ATTEMPTS) {
			return LoginStatus.SUSPENDED;

		} else if (!username.equals(this.username)) {
			loginAttempts++;
			return LoginStatus.BAD_NAME;

		} else if (!password.equals(this.password)) {
			loginAttempts++;
			return LoginStatus.BAD_PASS;

		} else {
			loginAttempts = 0;
			return LoginStatus.GOOD;
		}
	}

	/**
	 * Returns a string representation of this User instnace.
	 * 
	 * @return This User instance as a string
	 */
	public String toString() {
		return "Username: " + username + "\t Password: " + password;
	}

	/**
	 * TODO Do not actually delete this user
	 * 
	 * Deletes this user from the map of users.
	 * 
	 * @param username
	 *            The username of the user to remove
	 */
	public static void deleteUser(String username) {
		UserHash.remove(username);
	}

	/**
	 * @return the username
	 */
	public String getUsername() {
		return username;
	}

	/**
	 * Deletes all users from the system. This should only be used in a testing
	 * environment.
	 */
	public static void clear() {
		UserHash.clear();
	}
}
